import * as moment from "moment";
import { Position, Range, Selection, TextEditor, TextLine, WorkspaceEdit } from "vscode";
import { IDocGen } from "../../Common/IDocGen";
import { Config } from "../../Config";
import { CppArgument } from "./CppArgument";
import * as CppParser from "./CppParser";
import { CppParseTree } from "./CppParseTree";
import { CppToken, CppTokenType } from "./CppToken";

export enum SpecialCase {
    none,
    constructor,
    destructor,
    getter,
    setter,
    factoryMethod,
}

export enum CommentType {
    method,
    file,
}

export enum CasingType {
    Pascal,
    camel,
    snake,
    SCREAMING_SNAKE,
    UPPER,
    uncertain,
}

export class CppDocGen implements IDocGen {
    protected activeEditor: TextEditor;

    protected readonly cfg: Config;

    protected func: CppArgument;
    protected templateParams: string[];
    protected params: CppArgument[];

    protected specialCase: SpecialCase;
    protected commentType: CommentType;
    protected casingType: CasingType;

    protected smartTextLength: number;

    protected vscodeAutoGeneratedComment: boolean;

    /**
     * @param  {TextEditor} actEdit Active editor window
     * @param  {Position} cursorPosition Where the cursor of the user currently is
     * @param  {string[]} templateParams The template parameters of the declaration.
     * @param  {CppArgument} func The type and name of the function to generate doxygen.
     *                          Doesn't contain anything if it is not a function.
     * @param  {CppArgument[]} params The parameters of the function. Doesn't contain anything if it is not a function.
     * @param  {boolean} vscodeAutoGeneratedComment Set this to true if VS Code inserted an autogenerated comment closer
     *                                              on the next line after the comment.
     */
    public constructor(
        actEdit: TextEditor,
        cursorPosition: Position,
        cfg: Config,
        templateParams: string[],
        func: CppArgument,
        params: CppArgument[],
        specialCase: SpecialCase,
        commentType: CommentType,
        casingType: CasingType,
        vscodeAutoGeneratedComment: boolean,
    ) {
        this.activeEditor = actEdit;
        this.cfg = cfg;
        this.templateParams = templateParams;
        this.func = func;
        this.params = params;
        this.specialCase = specialCase;
        this.commentType = commentType;
        this.smartTextLength = 0;
        this.casingType = casingType;
        this.vscodeAutoGeneratedComment = vscodeAutoGeneratedComment;
    }

    /**
     * @inheritdoc
     */
    public GenerateDoc(rangeToReplace: Range) {
        let comment: string = "";
        if (this.commentType === CommentType.file) {
            comment = this.generateFileDescription();
        } else if (this.commentType === CommentType.method) {
            comment = this.generateComment();
        }

        // overwrite any autogenerated comment closer
        let modifiedRangeToReplace = rangeToReplace;
        if (this.vscodeAutoGeneratedComment) {
            const newPos: Position = new Position(
                modifiedRangeToReplace.end.line + 1,
                modifiedRangeToReplace.end.character,
            );
            modifiedRangeToReplace = new Range(rangeToReplace.start, newPos);
        }

        this.activeEditor.edit((editBuilder) => {
            editBuilder.replace(modifiedRangeToReplace, comment); // Insert the comment
        });

        // Set cursor to first DoxyGen command.
        this.moveCursurToFirstDoxyCommand(comment,
                                          modifiedRangeToReplace.start.line,
                                          modifiedRangeToReplace.start.character);
    }

    /***************************************************************************
                                    Implementation
     ***************************************************************************/
    protected getIndentation(): string {
        return this.activeEditor.document.lineAt(this.activeEditor.selection.start.line).text.match("^\\s*")[0];
    }

    protected getIndentedTemplate(replace: string): string {
        if (replace === undefined || replace === null || replace === "") {
            return "";
        }
        const snippets = replace.split(/({indent:\d+})/);

        let indentedString: string = "";
        let indentWidth: number = 0;

        // tslint:disable-next-line:prefer-for-of
        snippets.forEach((element) => {
            if (element.match(/{indent:\d+}/)) {
                const indents = parseInt(element.match(/{indent:(\d+)}/)[1], 10);
                indentWidth = indents;
                const numSpaces = Math.max(indentWidth - indentedString.length, 0);
                indentedString += " ".repeat(numSpaces);
            } else {
                // just some text
                indentedString += element;
            }
        });

        return indentedString;
    }

    protected getTemplatedString(replace: string, template: string, param: string): string {
        const replacedTemplate = template.replace(replace, param);
        return this.getIndentedTemplate(replacedTemplate);
    }

    protected getMultiTemplatedString(replace: string[], template: string, param: string[]): string {
        // For each replace entry, attempt to replace it with the corresponding param in the template
        for (let i = 0; i < replace.length; i++) {
            if (i < param.length) {
              template = template.replace(replace[i], param[i]);
            }
        }
        return template;
    }

    protected getSmartText(): string {
        if (!this.cfg.Generic.generateSmartText) {
            return "";
        }
        let val: string = "";
        let text: string = "";
        switch (this.specialCase) {
            case SpecialCase.constructor: {
                if (this.func.name === null) {
                    return "";
                } else {
                    const ctorText = this.func.name.trim();
                    this.casingType = CppParser.default.checkCasing(ctorText, 0);
                    val = this.splitCasing(ctorText).trim();
                    text = this.cfg.Cpp.ctorText;
                    break;
                }
            }
            case SpecialCase.destructor: {
                if (this.func.name === null) {
                    return "";
                } else {
                    const dtorText = this.func.name.replace("~", "").trim();
                    this.casingType = CppParser.default.checkCasing(dtorText, 0);
                    val = this.splitCasing(dtorText).trim();
                    text = this.cfg.Cpp.dtorText;
                    break;
                }
            }
            case SpecialCase.getter: {
                val = this.splitCasing(this.func.name.trim()).trim().substr(3).trim();
                text = this.cfg.C.getterText;
                break;
            }
            case SpecialCase.setter: {
                val = this.splitCasing(this.func.name.trim()).trim().substr(3).trim();
                text = this.cfg.C.setterText;
                break;
            }
            case SpecialCase.factoryMethod: {
                val = this.splitCasing(this.func.name.trim()).trim().substr(6).trim();
                text = this.cfg.C.factoryMethodText;
                break;
            }
            case SpecialCase.none:
            default: {
                return "";
            }
        }
        const str = this.getTemplatedString(this.cfg.nameTemplateReplace,
            text,
            val);
        this.smartTextLength = str.length;
        return str;
    }

    protected generateBrief(lines: string[]) {
        lines.push(
            ...this.getTemplatedString(
                this.cfg.textTemplateReplace,
                this.cfg.Generic.briefTemplate,
                this.getSmartText(),
            ).split("\n"),
        );
    }

    protected generateFromTemplate(lines: string[],
                                   replace: string,
                                   template: string,
                                   templateWith: string[]) {
        templateWith.forEach((element: string) => {
            // Ignore null values
            if (element !== null) {
                lines.push(...this.getTemplatedString(replace, template, element).split("\n"));
            }
        });
    }

    protected generateReturnParams(): string[] {
        if (this.cfg.Generic.includeTypeAtReturn === false) {
            return [""];
        }

        const params: string[] = [];

        // Check if return type is a pointer
        const ptrReturnIndex = this.func.type.nodes
            .findIndex((n) => n instanceof CppToken && n.type === CppTokenType.Pointer);

        // Special case for void functions.
        const voidReturnIndex = this.func.type.nodes
            .findIndex((n) => n instanceof CppToken && n.type === CppTokenType.Symbol && n.value === "void");

        // Special case for bool return type.
        const boolReturnIndex: number = this.func.type.nodes
            .findIndex((n) => n instanceof CppToken && n.type === CppTokenType.Symbol && n.value === "bool");

        if (boolReturnIndex !== -1 && this.cfg.Generic.boolReturnsTrueFalse === true) {
            params.push("true");
            params.push("false");
        } else if (voidReturnIndex !== -1 && ptrReturnIndex !== -1) {
            params.push(this.cfg.Generic.includeTypeAtReturn === true ? this.func.type.Yield() : "");
        } else if (voidReturnIndex === -1 && this.func.type.nodes.length > 0) {
            params.push(this.cfg.Generic.includeTypeAtReturn === true ? this.func.type.Yield() : "");
        }

        return params;
    }

    protected generateAuthorTag(lines: string[]) {
        if (this.cfg.Generic.authorTag.trim().length !== 0) {
            // Allow substitution of {author} and {email} only
            lines.push(
                ...this.getMultiTemplatedString(
                    [this.cfg.authorTemplateReplace, this.cfg.emailTemplateReplace],
                    this.cfg.Generic.authorTag,
                    [this.cfg.Generic.authorName, this.cfg.Generic.authorEmail],
                ).split("\n"),
            );
        }
    }

    protected generateFilenameFromTemplate(lines: string[]) {
        if (this.cfg.File.fileTemplate.trim().length !== 0) {
            this.generateFromTemplate(
                lines,
                this.cfg.nameTemplateReplace,
                this.cfg.File.fileTemplate,
                [this.activeEditor.document.fileName.replace(/^.*[\\\/]/, "")],
            );
        }
    }

    protected generateVersionTag(lines: string[]) {
        if (this.cfg.File.versionTag.trim().length !== 0) {
            lines.push(...this.cfg.File.versionTag.split("\n"));
        }
    }

    protected generateCopyrightTag(lines: string[]) {
        // This currently only supports year substitution
        this.cfg.File.copyrightTag.forEach((element) => {
            this.generateFromTemplate(
                lines,
                this.cfg.yearTemplateReplace,
                element,
                [moment().format("YYYY")],
            );
        });
    }

    protected generateCustomTag(lines: string[]) {
        let dateFormat: string = "YYYY-MM-DD"; // Default to ISO standard if not defined
        if ( this.cfg.Generic.dateFormat.trim().length !== 0) {
            dateFormat = this.cfg.Generic.dateFormat; // Overwrite with user format
        }
        // For each line of the customTag
        this.cfg.File.customTag.forEach((element) => {
            // Allow any of date, year, author, email to be replaced
            lines.push(
                ...this.getMultiTemplatedString(
                    [this.cfg.authorTemplateReplace, this.cfg.emailTemplateReplace,
                        this.cfg.dateTemplateReplace, this.cfg.yearTemplateReplace],
                    element,
                    [this.cfg.Generic.authorName, this.cfg.Generic.authorEmail,
                        moment().format(dateFormat), moment().format("YYYY")],
                ).split("\n"),
            );
        });
    }

    protected generateDateFromTemplate(lines: string[]) {
        if (this.cfg.Generic.dateTemplate.trim().length !== 0 &&
            this.cfg.Generic.dateFormat.trim().length !== 0) {
            this.generateFromTemplate(
                lines,
                this.cfg.dateTemplateReplace,
                this.cfg.Generic.dateTemplate,
                [moment().format(this.cfg.Generic.dateFormat)],
            );
        }
    }

    protected insertFirstLine(lines: string[]) {
        if (this.cfg.C.firstLine.trim().length !== 0) {
            lines.unshift(this.cfg.C.firstLine);
        }
    }

    protected insertBrief(lines: string[]) {
        if (this.cfg.Generic.briefTemplate.trim().length !== 0) {
            this.generateBrief(lines);
        }
    }

    protected insertLastLine(lines: string[]) {
        if (this.cfg.C.lastLine.trim().length !== 0) {
            lines.push(this.cfg.C.lastLine);
        }
    }

    protected generateFileDescription(): string {
        let lines: string[] = [];

        this.cfg.File.fileOrder.forEach((element) => {
            switch (element) {
                case "brief": {
                    this.insertBrief(lines);
                    break;
                }
                case "empty": {
                    lines.push("");
                    break;
                }
                case "file": {
                    this.generateFilenameFromTemplate(lines);
                    break;
                }
                case "version": {
                    this.generateVersionTag(lines);
                    break;
                }
                case "author": {
                    this.generateAuthorTag(lines);
                    break;
                }
                case "date": {
                    this.generateDateFromTemplate(lines);
                    break;
                }
                case "copyright": {
                    this.generateCopyrightTag(lines);
                    break;
                }
                case "custom": {
                    this.generateCustomTag(lines);
                    break;
                }
                default: {
                    break;
                }
            }
        });

        lines = lines.map((line) => `${this.cfg.C.commentPrefix}${line}`);
        this.insertFirstLine(lines);
        this.insertLastLine(lines);

        return lines.join("\n");
    }

    protected generateComment(): string {
        let lines: string[] = [];

        this.cfg.Generic.order.forEach((element) => {
            switch (element) {
                case "brief": {
                    this.insertBrief(lines);
                    break;
                }
                case "empty": {
                    lines.push("");
                    break;
                }
                case "tparam": {
                    if (this.cfg.Cpp.tparamTemplate.trim().length !== 0 && this.templateParams.length > 0) {
                        this.generateFromTemplate(
                            lines,
                            this.cfg.paramTemplateReplace,
                            this.cfg.Cpp.tparamTemplate,
                            this.templateParams,
                        );
                    }
                    break;
                }
                case "param": {
                    if (this.cfg.Generic.paramTemplate.trim().length !== 0 && this.params.length > 0) {
                        const paramNames: string[] = this.params.map((p) => p.name);
                        this.generateFromTemplate(
                            lines,
                            this.cfg.paramTemplateReplace,
                            this.cfg.Generic.paramTemplate,
                            paramNames,
                        );
                    }
                    break;
                }
                case "return": {
                    if (this.cfg.Generic.returnTemplate.trim().length !== 0 && this.func.type !== null) {
                        const returnParams = this.generateReturnParams();
                        // tslint:disable-next-line:max-line-length
                        this.generateFromTemplate(lines, this.cfg.typeTemplateReplace, this.cfg.Generic.returnTemplate, returnParams);
                    }
                    break;
                }
                default: {
                    break;
                }
            }
        });

        lines = lines.map((line) => `${this.cfg.C.commentPrefix}${line}`);
        this.insertFirstLine(lines);
        this.insertLastLine(lines);

        return lines.join(`\n${this.getIndentation()}`);
    }

    protected moveCursurToFirstDoxyCommand(comment: string, baseLine: number, baseCharacter) {
        // Find first offset of a new line in the comment. Since that's when the line where the first param starts.
        let line: number = baseLine;
        let character: number = comment.indexOf("\n");

        // If a first line is included find the 2nd line with a newline.
        if (this.cfg.C.firstLine.trim().length !== 0) {
            line++;
            const oldCharacter: number = character;
            character = comment.indexOf("\n", oldCharacter + 1) - oldCharacter;
        }

        // If newline is not found means no first param was found so Set to base line before the newline.
        if (character < 0) {
            line = baseLine;
            character = baseCharacter;
        }
        const to: Position = new Position(line, character);
        this.activeEditor.selection = new Selection(to, to);
    }

    protected splitCasing(text: string): string {
        if (!this.cfg.Generic.splitCasingSmartText) {
            return text;
        }
        let txt = text;
        let vals: string[] = [];
        switch (this.casingType) {
            case CasingType.SCREAMING_SNAKE: {
                txt = txt.toLowerCase();
            }
            case CasingType.snake: {
                vals = txt.split("_");
                break;
            }
            case CasingType.Pascal: {
                txt = txt.replace(/([A-Z0-9])/g, " $1");
                vals.push(txt);
                break;
            }
            case CasingType.camel: {
                txt = txt.replace(/([a-zA-Z0-9])(?=[A-Z])/g, "$1 ");
                vals.push(txt);
                break;
            }
            case CasingType.UPPER:
            case CasingType.uncertain:
            default: {
                return text;
            }
        }

        return vals.join(" ");
    }
}
